RESTier vNext: Modernize to ASP.NET Core, OData 8.x, and .NET 8/9/10#776
RESTier vNext: Modernize to ASP.NET Core, OData 8.x, and .NET 8/9/10#776jspuij wants to merge 512 commits into
Conversation
|
To start the conversation. Some opinionated choices have been made regarding the service registration and the new constructor signature for ApiBase which both eliminates the ServiceLocator anti-pattern and provides better extensibility. Service chaining has been decoupled from the underlying DI container and does not use reflection anymore (but an interface) to accomplish the chaining. The branch has switched to xUnit and FluentAssertions to better align with the other OData projects. BreakDance is still being used to setup the tests and generate API surfaces. Full OData.NET and AspNetCoreOdata support. dotnet 8, 9 and 10, although we are still waiting on the RTM versions of OData.NET and AspNetCoreOdata for dotnet 10, so it's forward compatibility for now. I might add some other fixes here and there for issues to this branch, while it's being reviewed. Full disclosure: The first part of this migration was done by hand with a little help of copilot. The last month I've been using Claude Code extensively. @robertmclaws shall we work together on this? What do you think. @gathogojr I see that you're involved now as well. Let's discuss. I've done previous upgrades of RESTier as well. |
|
@jspuij I just dropped you an email. Let's catch up this week and talk about it. |
Wires the new package into DotNetDocs SDK's source-project list so api-reference MDX gets auto-generated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Recommended OpenAPI page covering AddRestierNSwag, UseRestierOpenApi / UseRestierReDoc / UseRestierNSwagUI, the OpenApiConvertSettings configurator, multi-route discovery, and the combined-with-plain-MVC scenario. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n the dropdown
Previously the Swagger UI dropdown listed Restier routes only.
User-registered AddOpenApiDocument(...) docs were served at NSwag's
default /swagger/{name}/swagger.json path but did not appear in the UI.
This fix discovers all NSwag-registered documents at pipeline-config
time and appends them to the same SwaggerRoutes collection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the brainstormed design for issue OData#660: a chained IModelBuilder that maps standard .NET attributes to OData vocabulary annotations, flowing through to OpenAPI/Swagger output. Includes server-behavior implications (Computed/Immutable already drive the submit pipeline) and the integration tests that guard them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
17 TDD tasks covering the convention-based annotation builder, unit and integration tests (including submit-pipeline behavior assertions), and documentation updates with the new openapi-annotations.mdx page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hain Plumbing for issue OData#660. The builder is a no-op pass-through; subsequent commits add scanner logic per attribute family. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First scanner for the convention-based annotation builder. Establishes the fixtures + StaticInnerBuilder helper used by the rest of the unit test suite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the chain-preserving pattern from RestierWebApiModelBuilder so a custom inner builder returning an IEdmModel that is not an EdmModel flows through unchanged rather than collapsing the chain to null. Tightens an unused pattern variable, documents StubApi runtime constraint, and prunes unused usings in the test file. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an explicit test for complex-type description annotation. The existing scanner already handles this because complex types implement IEdmStructuredType; this test guards that invariant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the operation-method index built at constructor time, mirroring RestierWebApiOperationModelBuilder.ScanForOperations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Maps DatabaseGeneratedOption.Identity and DatabaseGeneratedOption.Computed to Core.V1.Computed = true. None is skipped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Maps RangeAttribute to Org.OData.Validation.V1.Minimum/Maximum, picking the constant expression type to match the target property's primitive kind (integer/floating/decimal). Non-numeric properties are logged and skipped rather than emitting a malformed annotation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Asserts that pre-existing annotations are preserved, that null inputs return null cleanly, that the constructor rejects null apiType, and that [MaxLength] does not produce a vocabulary annotation (the structural facet handles it). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Make explicit that the toggle also turns the /$count variant into a
404 instead of 200 OK { 0 } when the parent doesn't exist.
Mirrors the EFCore NoTrackingTests Breakdance suite onto the EF6 provider. Now feasible because the SQL Server fixture force-drop fix in 10d5d7a unblocked EF6 HTTP-level tests. Covers: * EF6 Default: GET applies AsNoTracking, surfaced in the executor's IQueryable expression as ObjectQuery.MergeAs(NoTracking). The substring "NoTracking" is the robust marker because EF6 lowers AsNoTracking into MergeAs rather than leaving a literal AsNoTracking method-call in the tree (no AsNoTrackingWithIdentityResolution equivalent on EF6). * EF6 TrackAll: GET leaves the DbSet bare (no NoTracking marker). * EF6 NoTracking explicit: same as Default. * EF6 Default + recursive $expand: falls back to tracked. Uses the cross- type cycle Publisher -> Book -> Publisher which the IExpandCycleDetector flags via QueryRequest.HasRecursiveExpand. Rooted from /Publishers (not /Books) to sidestep an unrelated NRE in the client-projection path for the seeded orphan "Sea of Rust" book whose Publisher nav is null. Refs: OData#726
SelectExpandHelper.ExecuteCoreAsync runs the OData-generated SelectExpand projection lambda in-memory as a workaround for the AspNetCoreOData OData#367 IEdmModel-constant bug. That lambda is generated SQL-style (no null guards) because EF6's legacy LINQ provider relies on LEFT JOIN + SQL null propagation. When executed in-memory the C# null semantics throw: e.g. /Books?$expand=Publisher($expand=Books) NREs on the seed's orphan "Sea of Rust" because book.Publisher.Books dereferences a null Publisher. Add a NullSafeMemberAccessRewriter (ExpressionVisitor) that wraps member access and instance/extension method calls on potentially-null reference receivers with "receiver == null ? default : ..." before compiling the lambda. Matches SQL-path semantics; the OData serializer emits the expanded slot as null for the orphan as expected. Add EF6 regression tests rooted at /Books so the orphan is included. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DeepInsert_ExceedsMaxDepth_Returns400 was failing because its services.AddSingleton<DeepOperationSettings>(...) inside the route IServiceCollection was silently replaced by AddRestierRoute, which applies the RestierRouteOptions bag after configureRouteServices. The request ran with the default MaxDepth=5, accepted the 2-level payload, and returned 201 instead of 400. Configure MaxDepth=1 through configureOptions (the canonical channel) in both DeepInsert_ExceedsMaxDepth_Returns400 and DeepInsert_MaxDepth1_AllowsOneLevel. The latter previously passed for the wrong reason — depth 1 fits both the intended limit and the unintended default of 5 — and now actually exercises MaxDepth=1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auto-maps EF6 / EF Core keyless DbSets to ComplexType + unbound FunctionImport returning Collection(ComplexType); RestierOperationExecutor gets a registry-based fallback that sources the underlying DbSet through api.QueryAsync so conventions still fire. Closes OData#741. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four substantive corrections: 1. v1 bypasses api.QueryAsync. ApiBase.QueryAsync only accepts QueryableSource<T>; routing keyless views through it requires IModelMapper + IQueryExpressionSourcer changes + composable-function- import resolution that don't exist yet. Executor returns the factory's IQueryable directly; AspNetCore.OData applies $filter / $select etc. at the OData layer. 2. OnFiltering<View> conventions do NOT fire in v1. The convention processor returns early unless the model reference is an IEdmEntitySet of IEdmEntityType. Function imports are explicitly listed as out-of-scope follow-up work. 3. Registry lives in Microsoft.Restier.Core and is lifetime-bridged manually by AddRestierRoute (mirroring the existing RestierWebApiModelExtender bridge across the modelBuildingServices dispose boundary). Constructor-injected into RestierOperationExecutor. 4. Writes return HTTP 405 via RestierController.Post's existing function-import branch, not 404. Test assertion updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-facing MDX page (guides/server/keyless-views.mdx), navigation update via the docsproj MintlifyTemplate, cross-links from model-building.mdx and operations.mdx, and a release-notes entry are now part of v1 — not a follow-up. Convention hooks and no-tracking are pulled out of the flat out-of-scope list into a dedicated Follow-ups section with explicit code-path-by-code-path scope (mapper, processor, sourcer, executor swap) so the deferred work is trackable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
21 tasks across 6 phases: - Phase 1: KeylessViewRegistry + lifetime bridge - Phase 2: shared model builder + EFCore partial - Phase 3: executor dispatch + EFCore end-to-end - Phase 4: EF6 model builder + tests - Phase 5: docs (new page, cross-links, navigation, release notes) - Phase 6: Swagger verification, follow-up issue, full test run Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. New Task 9b wires LibraryWithViewsContext seeding into the shared EF test helper (EFCore + EF6 branches). Without this the end-to-end tests would run against an empty database. 2. New Task 8b adds matching MethodNotAllowed guards to RestierController.Delete and the private Update method (PUT/PATCH). Today only Post has the function-import branch; DELETE/PUT/PATCH throw NotImplementedException, surfacing as HTTP 500. All four write verbs now return 405 consistently. Regression tests in Tasks 10 and 14 use a [Theory] over POST/PUT/PATCH/DELETE. 3. Task 4 step 2 now updates the two direct EFModelBuilder<IntegrationContext> ctor calls in EFModelBuilderSpatialIntegrationTests.cs (lines 42 and 61) so the new required KeylessViewRegistry parameter compiles. Verified by grep — those are the only direct construction sites in the repo. 4. Task 11 source-factory probe simplified to "property assignable to IQueryable<clrType>" — one check that covers DbSet<T>, IDbSet<T>, and DbQuery<T>. Spec edge-cases and the docs mapping table call out the discovery limitation: a property not configured via modelBuilder.Entity<T>() / EDMX won't appear in efEntityContainer.EntitySets and so isn't auto-mapped. Users must configure the entity in the model first regardless of property type. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. BooksByPublisher.PublisherId changed from int to string. The shared
Library fixture has Publisher.Id as string ("Publisher1",
"Publisher2"), so the planned view CLR types, the CREATE VIEW SQL,
the OData $filter literals ('Publisher1' instead of 1), and the
docs metadata sample now line up with the actual repo fixtures.
PublisherName dropped from the view to avoid EF6/EFCore OwnsOne
column-naming drift; BookCount narrowed from decimal to int.
2. EF6 LibraryWithViewsTestInitializer no longer calls a non-existent
SeedFor method. Task 12 step 3a refactors EF6 LibraryTestInitializer
to expose a public static SeedLibraryData(LibraryContext) under the
#if EF6 branch; the new initialiser delegates to that helper. The
protected Seed override stays in place and now calls
SeedLibraryData itself. EFCore needs no refactor — its Seed is
already public via IDatabaseInitializer.
3. File inventory now describes the source-factory map correctly as
Dictionary<string, Func<object, IQueryable>> (keyed by entity-set
name) instead of Dictionary<Type, ...>. Spec and plan now match
the implementation steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…otNullOrEmpty) The Ensure helper in src/Microsoft.Restier.Core/Helpers/Ensure.cs only exposes NotNullOrWhiteSpace; NotNullOrEmpty does not exist. Caught before dispatching Task 1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Also restores the <see cref="KeylessViewRegistry"/> in KeylessViewEntry's summary doc comment, which had to be downgraded to <c>...</c> in the previous commit because the type didn't exist yet. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Registers KeylessViewRegistry in the temporary model-building service provider, captures the populated instance before disposal, then re-registers the same instance into the per-route services lambda. Mirrors the existing RestierWebApiModelExtender pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates the two direct-construction call sites in the EFCore Spatial integration tests to pass a fresh KeylessViewRegistry. Production code gets the registry via DI through the lifetime bridge in AddRestierRoute (see prior commit). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pipes a Dictionary<string, Func<object, IQueryable>> source-factory map from the EFCore partial through the shared GetEdmModel. The shared BuildEdmModelFromEntitySetMaps now splits the entity-set map into keyed entity sets and keyless view sets: - keyed entries proceed through the existing EntitySet<T> path - keyless entries are registered as ComplexType<T> and get an unbound FunctionImport added to the EdmEntityContainer post-build, returning Collection(<ComplexType>); they are also recorded in KeylessViewRegistry alongside their source factory so the request-time dispatch path (RestierOperationExecutor, next phase) can find them. Empty key lists are normalised to 'keyless' so the EF6 path (which produces empty lists rather than nulls for keyless entity sets) lands in the same branch when Task 11 wires up its source-factory map. EF6 build is intentionally broken at this commit - its partial's EntityFramework6GetEntitySets still has the old 2-out signature. Task 11 adds the matching 3-out signature. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Flips the existing 'should throw on keyless' assertion to verify the new auto-mapping behaviour. Adds a mixed-model test asserting regular entity sets coexist with a keyless view. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the reflective method lookup on the API returns null, consult KeylessViewRegistry. On hit, invoke the source factory and return its IQueryable directly so AspNetCore.OData can apply query options at the OData layer. On miss, throw the existing NotImplementedException unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the existing 405 branch in Post. Without this, DELETE/PUT/PATCH on a function-import URL (e.g. a keyless-view import) threw NotImplementedException, surfacing as HTTP 500. Now all four write verbs return 405 Method Not Allowed consistently the desired UX for a read-only resource. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Promotes BooksByPublisher, LibraryWithViewsContext, and LibraryWithViewsApi into Tests.Shared.EntityFrameworkCore so the AspNetCore regression tests can reference them. The DbContext gains an IsConfigured-guarded OnConfiguring fallback to in-memory so model-shape tests still work without a provider being supplied, while end-to-end tests can route through a relational provider via the user-secret connection string. Adds an instrumented OnFilteringBooksByPublisher method to assert the v1 limitation (convention does not fire for keyless-view function imports). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Core) EFCore: SeedDatabase<LibraryWithViewsContext, LibraryWithViewsTestInitializer> runs after AddEFCoreProviderServices, populating publishers/books from the existing LibraryTestInitializer then creating the BooksByPublisher SQL view on top. EF6 path will be wired in Task 12 (creates the EF6 LibraryWithViewsContext plus its DropCreateDatabaseAlways initialiser). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures a Dictionary<string, Func<object, IQueryable>> alongside the entity-set and key maps. Source-factory selection: prefer reflection on a context property whose type is assignable to IQueryable<T> (covers DbSet<T>, IDbSet<T>, DbQuery<T>); fall back to ObjectContext.CreateQuery with an ESQL '[Container].[EntitySet]' string for EDMX-only entity sets. Empty KeyProperties lists are now normalised (filtering null elements) so the shared builder treats them as keyless and routes them to the ComplexType + FunctionImport path. Also fixes a collateral break from Task 8 (RestierOperationExecutor ctor gained a KeylessViewRegistry parameter): RestierOperationExecutorTests constructed the executor directly and needed the new arg. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issue741_KeylessViews:
- GET /BooksByPublisher() returns 200 with rows
- \$filter=PublisherId eq 'Publisher1' narrows correctly
- OnFilteringBooksByPublisher call count stays at 0 (pins v1 limitation
that convention hooks don't fire for keyless-view function imports;
see Follow-up A in the spec)
- POST/PUT/PATCH/DELETE all return HTTP 405
Tests require ConnectionStrings:LibraryWithViewsContext to be set in
the test project's user-secrets, e.g.:
cd test/Microsoft.Restier.Tests.Shared.EntityFrameworkCore
dotnet user-secrets set "ConnectionStrings:LibraryWithViewsContext" \
"Server=...;Database=LibraryWithViewsContext;..."
The LibraryWithViewsTestInitializer (committed in 09d64431) populates
publishers/books via LibraryTestInitializer.Seed then creates the
BooksByPublisher SQL view via ExecuteSqlRaw.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ture
Replaces the dedicated LibraryWithViewsContext / LibraryWithViewsTestInitializer
with a BooksByPublisher DbSet directly on the shared LibraryContext (EFCore
only). Saves a user-secret connection string and reduces ongoing fixture
maintenance.
Substantive changes:
- BooksByPublisher CLR type lives in Tests.Shared/Scenarios/Library/ (no EF
attribute, fully TFM-agnostic).
- LibraryContext gets a `DbSet<BooksByPublisher>` and fluent
`HasNoKey().ToView("BooksByPublisher")` under #if EFCore. EF6 code-first
doesn't support keyless entity types, so EF6 testing of keyless views
remains a separate concern (the EF6 source-factory work in
src/Microsoft.Restier.EntityFramework/Model/EfModelBuilder.cs still covers
the EDMX-defined-keyless-entity-set case for production EF6 users).
- LibraryTestInitializer.Seed creates the BooksByPublisher SQL view via
ExecuteSqlRaw, guarded by IsRelational() so in-memory-provider tests
(Issue704, Issue519 etc.) don't trip on the relational-only API.
- The EdmFunction emitted by AddKeylessViewFunctionImports moves to a
"<namespace>.Views" sub-namespace. The previous code put the function in
the same namespace as the ComplexType (with the same name), which violated
the OData CSDL uniqueness rule for schema-level elements — caught when the
full AspNetCore test suite started failing model validation. The
FunctionImport name (and so the URL `GET /odata/<view>()`) is unchanged.
- LibraryWithViewsApi keeps existing — slimmed down to inherit from
EntityFrameworkApi<LibraryContext>; its only job is to host the
instrumented OnFilteringBooksByPublisher probe used by the regression
test to pin the v1 limitation.
- Deletes LibraryWithViewsContext, LibraryWithViewsTestInitializer, and the
matching `else if` branch in the shared EF helper.
- Refreshes the LibraryApi-EFCore-ApiMetadata.txt baseline to include the
new ComplexType + FunctionImport + Views schema.
Verified: Tests.Core 345/345, Tests.EntityFrameworkCore 10/10, and
Tests.AspNetCore 451/451 (+4 skipped pre-existing) all green on net9.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EF6 can't host keyless entity types in code-first (model validation rejects entities without a key), and the EDMX-defined-keyless-entity-set path is explicitly out of scope. EF6 users who want view-shaped read-only resources continue to hand-author [UnboundOperation] methods. Substantive changes: - src/Microsoft.Restier.EntityFramework/Model/EfModelBuilder.cs: reverts the EDMX/source-factory work added in 55cf33ca. The EF6 partial now throws InvalidOperationException with a clear "keyless not supported on EF6, use EF Core" message if it encounters an entity set with empty KeyProperties. Restores the original 2-out method signature. - src/Microsoft.Restier.EntityFramework.Shared/Model/EFModelBuilder.cs: EF6 branch of GetEdmModel synthesises an empty sourceFactoryMap so the shared BuildEdmModelFromEntitySetMaps signature stays compatible. Spec + plan: - Spec Goal / Decisions / Components tables updated to EFCore-only. - EF6 sections in the Edge cases and Testing tables collapsed to a single "Not supported" row pointing at hand-authored UnboundOperation. - Spec Follow-up C is now an explicit EF6-support follow-up (the attribute + SqlQuery escape hatch) deferred until real demand emerges. - Plan: Phase 4 (Tasks 11-14, ~550 lines) replaced with a brief stub explaining the scope change. File inventory updated. - MDX page outline: Tabs collapsed to a single EF Core code sample; EF6 callout added as a Note. Mapping table rewritten. Tests: Tests.Core 345/345, Tests.EntityFramework 7/7, Tests.EntityFrameworkCore 10/10, Tests.AspNetCore 451/451 (+4 skipped pre-existing). All green on net9.0. Solution builds clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- guides/server/keyless-views.mdx: new user-facing page covering when the feature applies, the auto-generated EDM shape, query examples, v1 limitations callout, and the EF6 not-supported Note. - model-building.mdx: short paragraph in "Customizing the Entity Model" pointing to the keyless-views page; replaces the older "if you're using Model First + SQL Views you'll need a PK" hand-wave. - operations.mdx: adds a Note that auto-generated function imports for keyless views aren't hand-authored with [UnboundOperation]. - docsproj MintlifyTemplate: keyless-views added to the Server nav group; vnext release-notes stub added to the Release Notes nav. - release-notes/vnext.md: placeholder entry for the keyless-views feature; user renames / merges into a versioned file at release-cut time. - docs.json: regenerated by the docsproj build. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…iour Two findings from review: 1. (High) Keyless-view dispatch bypassed PerformPreEvent / PerformPostEvent in RestierOperationExecutor. Any IOperationFilter implementations (auditing, metrics, mutation, validation) silently stopped applying to auto-generated function imports while still running for hand- authored [UnboundOperation] methods. Fixed: wrap the SourceFactory call in the same pre/post filter calls the normal method path uses. 2. (Medium-as-reported / false-alarm-in-practice) The original concern was that function-import names weren't lower-camel-cased under RestierNamingConvention.LowerCamelCase. Empirical check of the metadata under that convention shows ODataConventionModelBuilder. EnableLowerCamelCase() only lower-camel-cases *property* and *enum-member* names — NOT container-level names like EntitySet, Singleton, FunctionImport. So keyless-view function imports staying PascalCase is the correct behaviour, matching how regular entity sets surface. Added EFModelBuilder_LowerCamelCase_KeylessViewImport_MatchesEntitySetCasing to pin this: under LowerCamelCase, EntitySet Name="Books" + Property Name="isbn", and FunctionImport Name="BooksByPublisher" (PascalCase). Comment in AddKeylessViewFunctionImports calls out the convention. Verified: AspNetCore 451/451, EFCore 11/11 (the new test added), warning-clean build across net8/9/10. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match the normal-path invariant — RestierOperationContext.ParameterValues is set to a non-null array before PerformPreEvent runs. Keyless-view function imports have no parameters, so the value is Array.Empty<object>(). Custom IOperationFilter implementations can now read context.ParameterValues without null-guarding, just like they can on hand-authored [UnboundOperation] methods. Verified by ExecuteOperationAsync_KeylessView_Invokes_Filters_With_NonNull_ParameterValues: captures the array the filter sees when OnOperationExecutingAsync runs and asserts non-null + empty. Sanity-checked by temporarily reverting the assignment — test correctly fails with "Expected capturedParameterValues not to be <null>" — then restored. AspNetCore 452/452 green on net9.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
This PR is the cumulative result of the RESTier vNext effort — a ground-up modernization of the framework to align with current .NET, OData, and ASP.NET Core ecosystems. It spans 521 changed files across architecture, platform support, testing infrastructure, documentation, and samples.
Platform & dependency upgrades
Architecture changes
Removed legacy ASP.NET (System.Web) support
The
Microsoft.Restier.AspNetproject and its shared project (AspNet.Shared) have been removed. RESTier is now exclusively an ASP.NET Core framework.New dynamic routing system
Replaced the 8-file template-based OData routing convention system with a single
RestierRouteValueTransformerthat uses ASP.NET Core'sDynamicRouteValueTransformerfor dynamic OData path parsing. A newMapRestier()endpoint route builder extension provides the public API, withRestierRouteMarkeras a sentinel service for route identification.Redesigned DI and initialization API
AddRestier()/MapRestier()registration surface usingMicrosoft.Extensions.DependencyInjectionIChainedService<T>with automaticInnerproperty injectionRelocated model building
Model builders (
RestierWebApiModelBuilder,RestierWebApiModelExtender,RestierWebApiOperationModelBuilder,RestierWebApiModelMapper) moved from the removed shared project intoMicrosoft.Restier.AspNetCoreunderModel/ApiExtension/.Swagger / OpenAPI rewrite
Ported
Microsoft.Restier.AspNetCore.Swaggerfrom the legacy Swashbuckle provider model to ASP.NET Core's built-in OpenAPI middleware (RestierOpenApiDocumentGenerator+RestierOpenApiMiddleware), compatible with Swashbuckle 10.x.Bug fixes
PathBaseinBuildBaseAddressto prevent double-slash URLs$metadataand service document endpoints; includePathBasein base address$countcombined with$select/$expand; implementFilterSegmenthandler inRestierQueryBuilder; work around OData v9$expand/$selectincompatibility with EF6ODataPathIListcast inGetPathKeyValuesTestSetupinfinite recursion bug in Breakdance 8.0mainNew features
TimeOnlyfor EFCoreTimeOfDayconverter and provider-specific metadata baselinesRestierQueryBuildernow handles$filteras a path segment (OData 4.01)Microsoft.Restier.Samples.Postgres.AspNetCoreproject demonstrating EF Core + Npgsql with migrations and seed dataRestierNamingConventionparameter onAddRestierRoute. Three modes:PascalCase(default),LowerCamelCase(properties only), andLowerCamelCaseWithEnumMembers(properties + enum members). Implemented end-to-end across model building, serialization, deserialization (RestierResourceDeserializer), query options, ETag/concurrency handling (NormalizePropertyNames), and enum parsing. Property name mapping handled by newEdmClrPropertyMapperutility. Per-route configuration allows different naming conventions on different API routes.Testing infrastructure overhaul
src/totest/directoryTests.Legacy,Tests.Breakdance,Tests.AspNet,Tests.AspNetCorePlusEF6)Tests.Shared,Tests.Shared.EntityFramework,Tests.Shared.EntityFrameworkCoredotnet user-secrets. Thread-safe database seeding prevents race conditions in parallel test runs.$select,$filter,$expand,$orderby), POST, PATCH, PUT, DELETE, ETag concurrency, and enum handling for bothLowerCamelCaseandLowerCamelCaseWithEnumMembersmodesInternalsVisibleToauto-configured from source to matching test projectDocumentation
Complete rewrite of the
docs/msdocs/documentation to reflect the vNext API:Build & project structure
.slnxformat (RESTier.slnx)Directory.Build.propsand.editorconfigmoved fromsrc/to repository rootrestier.snk) moved to repository rootTest plan
dotnet build RESTier.slnxsucceeds on all target frameworks (net8.0, net9.0, net10.0)dotnet test RESTier.slnx— all tests pass (xUnit v3)LowerCamelCaseandLowerCamelCaseWithEnumMembersmodes$metadata, service document,$filterpath segment all resolve correctly